<?php


namespace App\Service\Dispatch;

use App\Constants\XZXResponseCode;
use App\Model\Dao\ChannelInfoDao;
use App\Model\Enum\ChannelCollectionMchConnStatusEnum;
use App\Model\Enum\ChannelInfoStatusEnum;
use App\Utils\CustomUtils;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Utils\Arr;
use function PHPUnit\Framework\arrayHasKey;

/**
 * Class ChannelDispatchService
 * 通道调度
 *
 * @package App\Service\Dispatch
 */
class ChannelDispatchService {

    /**
     * @var ChannelInfoDao
     * @Inject()
     */
    protected $ChannelInfoDao;

    public function handle ($mchNo, $bankTransList) {

        [$year, $month, $day] = [date('Y'), date('m'), date('d')];


        // 获取通道限额
        $bankTransList = array_column($bankTransList, null, 'channelNo');
        $channelNoList = array_keys($bankTransList);
        $channelList   = $this->_getChannelLimitList($channelNoList);

        // 获取商户终端限额
        $mchChannelList = $this->_getMchChannelLimitList($mchNo);

        // 获取有效的channelNo列表
        $channelNoList = $this->_getAvailableChannelNoList($bankTransList, $channelList, $mchChannelList);
        if (empty($channelNoList)) {
            return CustomUtils::buildError(trans('dispatch.channel.no_available'));
        }

        // 遍历提交的通道列表
        $availableChannelList = [];
        $sortedChannelList    = [];
        $failChannelList      = [];

        foreach ($channelNoList as $channelNo) {

            $channel    = $channelList[$channelNo];
            $mchChannel = $mchChannelList[$channelNo];
            $bankTrans  = $bankTransList[$channelNo];

            // 通道/商户通道未开启
            if ($channel['status'] != ChannelInfoStatusEnum::ON) {
                array_push($failChannelList, [
                    'mchNo'     => $mchChannel['mchNo'],
                    'channelNo' => $channel['channelNo'],
                    'reason'    => trans('dispatch.channel.limit.not_open', ['channel_no' => $channelNo]),
                ]);
                continue;
            }
            if ($mchChannel['status'] != ChannelCollectionMchConnStatusEnum::ON) {

                array_push($failChannelList, [
                    'mchNo'     => $mchChannel['mchNo'],
                    'channelNo' => $mchChannel['channelNo'],
                    'reason'    => trans('dispatch.mch.channel.limit.not_open', [
                        'mch_no'     => $mchChannel['mchNo'],
                        'channel_no' => $mchChannel['channelNo'],
                    ]),
                ]);
                continue;
            }

            $bankTrans = array_merge($bankTrans, $mchChannel);

            // var_dump($bankTrans);

            // 结合上面两个列表,筛选满足本次交易的通道列表
            try {

                // 通道限额判断
                $this->_checkChannelLimit($mchNo, $bankTrans['channelLimitAmount'], $mchChannel, $channel);

                $availableChannelList[$channelNo] = $bankTrans;

                // 加入到待调度列表,按照优先级分组
                if (!Arr::exists($sortedChannelList, $bankTrans['sort'])) {
                    $sortedChannelList[$bankTrans['sort']] = [];
                }

                array_push($sortedChannelList[$bankTrans['sort']], $bankTrans);

            } catch (\Exception $e) {

                // 调整通道状态
                $this->_autoCloseExcessChannel($mchChannel, $channel);

                array_push($failChannelList, [
                    'mchNo'     => $mchChannel['mchNo'],
                    'channelNo' => $mchChannel['channelNo'],
                    'reason'    => $e->getMessage(),
                ]);

                continue;
            }
        }

        CustomUtils::xzxDebug('不可用通道', $failChannelList);

        if (empty($availableChannelList)) {
            return CustomUtils::buildError(trans('dispatch.channel.no_available'), XZXResponseCode::FAILED_CODE, [
                'failChannelList' => $failChannelList,
            ]);
        }

        // 调度
        $channelNo = $this->_dispatchBestChannel($sortedChannelList, $mchNo);

        $transChannel = $availableChannelList[$channelNo];
        $mchChannel   = $mchChannelList[$channelNo];
        $channel      = $channelList[$channelNo];

        // 更新已用额度
        $this->_updateChannelLimitCost($mchNo, $transChannel['channelLimitAmount'], $mchChannel, $channel);

        // 调整通道状态
        $this->_autoCloseExcessChannel($mchChannel, $channel);

        $data = [
            'collectionId'         => $transChannel['collectionId'],
            'channelId'            => $transChannel['channelId'],
            'channelNo'            => $transChannel['channelNo'],
            'channelName'          => $transChannel['channelName'],
            'channelTransCurrency' => $transChannel['channelTransCurrency'],
            'channelTransAmount'   => $transChannel['channelTransAmount'],
            'channelLimitCurrency' => $transChannel['channelLimitCurrency'],
            'channelLimitAmount'   => $transChannel['channelLimitAmount'],
            'year'                 => $year,
            'month'                => $month,
            'day'                  => $day,
            'failChannelList'      => $failChannelList,
        ];

        return CustomUtils::buildSuccess($data);
    }

    /**
     * 判断三个通道列表的通道编号的交集
     *
     * @param $bankTransList  array 提交过来的通道列表
     * @param $channelList    array 总通道限额列表
     * @param $mchChannelList array 商户-通道限额列表
     *
     * @return array
     */
    protected function _getAvailableChannelNoList ($bankTransList, $channelList, $mchChannelList) {

        if (empty($bankTransList)) {
            return [];
        }

        if (empty($channelList)) {
            return [];
        }

        if (empty($mchChannelList)) {
            return [];
        }

        $a = array_column($bankTransList, 'channelNo');
        $b = array_column($channelList, 'channelNo');
        $c = array_column($mchChannelList, 'channelNo');

        // 返回交集
        return array_intersect($a, $b, $c);
    }

    /**
     * 金额限制判断
     * 此处金额比较无需考虑阈值设定,因为是单笔限额,只要单笔不超过金额限制就行
     * 且允许二值恰好相等
     * 但是要考虑阈值设定是否为0
     *
     * @param int   $mchNo      商户号
     * @param int   $amount     交易金额
     * @param array $mchChannel 商户通道限额
     * @param array $channel    通道限额
     *
     * @throws \Exception
     */
    protected function _checkChannelLimit ($mchNo, $amount, $mchChannel, $channel) {

        $channelNo   = $channel['channelNo'];
        $channelName = $channel['channelName'];
        $currency    = $channel['settleCurrency'];

        // 商户侧 ---------------------------------

        // 商户通道单笔最小金额限制
        if ($mchChannel['amountMin'] > 0) {
            if ($amount < $mchChannel['amountMin']) {
                throw new \Exception(trans('dispatch.mch.channel.limit.not_satisfied.amount_min', [
                    'currency' => $currency,
                    'amount'   => sprintf('%.2f', $mchChannel['amountMin'] / 100),
                ]));
            }
        }
        else {
            throw new \Exception(trans('dispatch.mch.channel.limit.invalid.amount_min'));
        }

        // 商户通道单笔最大金额限制
        if ($mchChannel['amountLimit'] > 0) {
            if (!$this->ChannelInfoDao->checkLimit($amount, $mchChannel['amountLimit'], true)) {
                throw new \Exception(trans('dispatch.mch.channel.limit.not_satisfied.amount_max', [
                    'currency' => $currency,
                    'amount'   => sprintf('%.2f', $mchChannel['amountLimit'] / 100),
                ]));
            }
        }
        else {
            throw new \Exception(trans('dispatch.mch.channel.limit.invalid.amount_max'));
        }

        // 商户通道金额限制(日)
        if ($mchChannel['dayAmountThreshold'] > 0) {
            if (!$this->ChannelInfoDao->checkLimit($amount + $this->ChannelInfoDao->readMchChannelAmount($mchNo, $channelNo, 'day'), $mchChannel['dayAmountLimit'], true)) {
                throw new \Exception(trans('dispatch.mch.channel.limit.not_satisfied.daily_amount', [
                    'currency' => $currency,
                    'amount'   => sprintf('%.2f', $mchChannel['dayAmountLimit'] / 100),
                ]));
            }
        }
        else {
            throw new \Exception(trans('dispatch.mch.channel.limit.invalid.daily_amount_threshold'));
        }

        // 商户通道金额限制(月)
        if ($mchChannel['monthAmountThreshold'] > 0) {
            if (!$this->ChannelInfoDao->checkLimit($amount + $this->ChannelInfoDao->readMchChannelAmount($mchNo, $channelNo, 'month'), $mchChannel['monthAmountLimit'], true)) {
                throw new \Exception(trans('dispatch.mch.channel.limit.not_satisfied.monthly_amount', [
                    'currency' => $currency,
                    'amount'   => sprintf('%.2f', $mchChannel['monthAmountLimit'] / 100),
                ]));
            }
        }
        else {
            throw new \Exception(trans('dispatch.mch.channel.limit.invalid.monthly_amount_threshold'));
        }

        // 通道侧 ---------------------------------

        // 通道单笔最小金额限制
        if ($channel['amountMin'] > 0) {
            if ($amount < $channel['amountMin']) {
                throw new \Exception(trans('dispatch.channel.limit.not_satisfied.amount_min', [
                    'currency' => $currency,
                    'amount'   => sprintf('%.2f', $channel['amountMin'] / 100),
                ]));
            }
        }
        else {
            throw new \Exception(trans('dispatch.channel.limit.invalid.amount_min'));
        }

        // 通道单笔最大金额限制
        if ($channel['amountLimit'] > 0) {
            if (!$this->ChannelInfoDao->checkLimit($amount, $channel['amountLimit'], true)) {
                throw new \Exception(trans('dispatch.channel.limit.not_satisfied.amount_max', [
                    'currency' => $currency,
                    'amount'   => sprintf('%.2f', $channel['amountMin'] / 100),
                ]));
            }
        }
        else {
            throw new \Exception(trans('dispatch.channel.limit.invalid.amount_max'));
        }

        // 通道金额限制(日)
        if ($channel['dayCountLimit'] > 0 && $channel['dayAmountLimit'] > 0 && $channel['dayAmountThreshold'] > 0) {
            if (!$this->ChannelInfoDao->checkLimit($amount + $this->ChannelInfoDao->readChannelAmount($channelNo, 'day'), $channel['dayAmountLimit'], true)) {
                throw new \Exception(trans('dispatch.channel.limit.invalid.daily_amount'));
            }
        }
        else {
            throw new \Exception(trans('dispatch.channel.limit.invalid.daily_limit'));
        }

        // 通道金额限制(月)
        if ($channel['monthCountLimit'] > 0 && $channel['monthAmountLimit'] > 0 && $channel['monthAmountThreshold'] > 0) {
            if (!$this->ChannelInfoDao->checkLimit($amount + $this->ChannelInfoDao->readChannelAmount($channelNo, 'month'), $channel['monthAmountLimit'], true)) {
                throw new \Exception(trans('dispatch.channel.limit.invalid.monthly_amount'));
            }
        }
        else {
            throw new \Exception(trans('dispatch.channel.limit.invalid.monthly_limit'));
        }

    }

    /**
     * @param $mchNo
     * @param $amount
     * @param $mchChannel
     * @param $channel
     *
     * @return array
     */
    protected function _updateChannelLimitCost ($mchNo, $amount, $mchChannel, $channel) {

        $channelNo = $mchChannel['channelNo'];

        // 更新额度:商户侧/通道侧,日周期/月周期,次数/金额
        $this->ChannelInfoDao->incrChannelLimitCost($mchNo, $channelNo, $amount, 1, 'day');
        $this->ChannelInfoDao->incrChannelLimitCost($mchNo, $channelNo, $amount, 1, 'month');
    }

    /**
     * 自动检测通道是否超额并关闭
     *
     * @param array $mchChannel 商户通道
     * @param array $channel    通道
     */
    protected function _autoCloseExcessChannel ($mchChannel, $channel) {

        if ($this->ChannelInfoDao->isChannelExcess($channel)) {

            // 通道超额,关闭通道
            $this->_closeChannel($channel['channelNo']);

        }
        else if ($this->ChannelInfoDao->isMchChannelExcess($mchChannel)) {

            // 商户通道超额,关闭商户通道
            $this->_closeMchChannel($mchChannel['mchNo'], $mchChannel['channelNo']);

        }
    }

    /**
     * 通道调度
     *
     * @param array  $sortedChannelList 按照优先级分组的通道列表
     * @param string $mchNo             商户号
     *
     * @return string 通道编码
     */
    protected function _dispatchBestChannel ($sortedChannelList, $mchNo) {

        // 按照键名降序
        krsort($sortedChannelList);

        $channel     = null;
        $channelList = reset($sortedChannelList);

        if (count($channelList) == 1) {
            $channel = reset($channelList);
        }
        else {
            // 多个,轮询调度
            $total = count($channelList);

            // 待调度通道按照channelNo升序
            $channelList = array_column($channelList, null, 'channelNo');
            ksort($channelList);
            $channelList = array_values($channelList);

            // 读取当前轮询调度下标
            $currPos = $this->ChannelInfoDao->readDispatchPollingPos($mchNo);
            $nextPos = ($currPos + 1) % $total;
            $channel = $channelList[$nextPos];

            // 写入当前轮询调度下标
            $this->ChannelInfoDao->writeDispatchPollingPos($mchNo, $nextPos);

            CustomUtils::xzxDebug([
                '通道编号列表: '.json_encode(array_column($channelList, 'channelNo'), JSON_UNESCAPED_UNICODE),
                '通道名称列表: '.json_encode(array_column($channelList, 'channelName'), JSON_UNESCAPED_UNICODE),
                '轮询下标: '.$mchNo.' : '.$nextPos,
            ]);
        }

        $channelNo = $channel['channelNo'];

        return $channelNo;
    }

    /**
     * 关闭商户通道
     *
     * @param $mchNo
     * @param $channelNo
     */
    protected function _closeMchChannel ($mchNo, $channelNo) {
        // 清除缓存,下次获取的时候会判断
        $this->clearMchChannelCache($mchNo);
    }

    /**
     * 清除商户通道限额列表缓存
     *
     * @param $mchNo
     */
    public function clearMchChannelCache ($mchNo) {
        $this->ChannelInfoDao->clearMchChannelLimitList($mchNo);
    }

    public function clearChannel ($channelNo) {
        $this->ChannelInfoDao->clearChannel($channelNo);
    }

    /**
     * 关闭通道
     *
     * @param $channelNo
     */
    protected function _closeChannel ($channelNo) {

        if (is_array($channelNo)) {
            foreach ($channelNo as $_channelNo) {
                $this->_closeChannel($_channelNo);
            }
        }
        else {
            // 清除缓存,下次获取的时候重新判断
            $this->clearChannel($channelNo);
        }

    }

    /**
     * 开启/关闭通道
     *
     * @param $mchNo
     * @param $channelNo
     * @param $status
     *
     * @return array
     */
    public function setMchChannelStatus ($mchNo, $channelNo, $status) {

        $channelLimitList = $this->_getMchChannelLimitList($mchNo);

        // 通道关闭(手动关闭或禁用)
        foreach ($channelLimitList as $k => $channel) {
            if ($channel['channelNo'] == $channelNo) {

                $channel['status'] = $status;

                if ($status == ChannelInfoStatusEnum::ON) {
                    $channel['statusTxt'] = '';
                }
                else {
                    $channel['statusTxt'] = '人工关闭';
                }

                $channelLimitList[$k] = $channel;

                break;
            }
        }

        // 这边是只操作redis
        $this->ChannelInfoDao->writeMchChannelLimitList($mchNo, $channelLimitList);

        return CustomUtils::buildSuccess();

    }

    /**
     * @param $channelNo
     *
     * @return array
     */
    public function getChannelCost ($channelNo) {

        $dayCurrentCount    = $this->ChannelInfoDao->readChannelCount($channelNo, 'day');
        $monthCurrentCount  = $this->ChannelInfoDao->readChannelCount($channelNo, 'month');
        $dayCurrentAmount   = $this->ChannelInfoDao->readChannelAmount($channelNo, 'day');
        $monthCurrentAmount = $this->ChannelInfoDao->readChannelAmount($channelNo, 'month');

        $channelLimitList = $this->_getChannelLimitList([$channelNo]);

        $status = ChannelInfoStatusEnum::ON;
        foreach ($channelLimitList as $k => $channel) {
            if ($channel['channelNo'] == $channelNo) {
                $status = $channel['status'];
                break;
            }
        }
        $statusTxt = ChannelInfoStatusEnum::born($status)->label();

        $data = [
            'channelNo'          => $channelNo,
            'status'             => $status,
            'statusTxt'          => $statusTxt,
            'dayCurrentCount'    => $dayCurrentCount,
            'dayCurrentAmount'   => $dayCurrentAmount,
            'monthCurrentCount'  => $monthCurrentCount,
            'monthCurrentAmount' => $monthCurrentAmount,
            'day'                => date('Y-m-d'),
            'month'              => date('Ym'),
        ];

        return CustomUtils::buildSuccess($data);
    }

    /**
     * @param $mchNo
     * @param $channelNo
     *
     * @return array
     */
    public function getMchChannelCost ($mchNo, $channelNo) {

        $dayCurrentCount    = $this->ChannelInfoDao->readMchChannelCount($mchNo, $channelNo, 'day');
        $monthCurrentCount  = $this->ChannelInfoDao->readMchChannelCount($mchNo, $channelNo, 'month');
        $dayCurrentAmount   = $this->ChannelInfoDao->readMchChannelAmount($mchNo, $channelNo, 'day');
        $monthCurrentAmount = $this->ChannelInfoDao->readMchChannelAmount($mchNo, $channelNo, 'month');

        $channelLimitList = $this->_getMchChannelLimitList($mchNo);

        // 读取商户通道限额状态
        $status = ChannelInfoStatusEnum::ON;
        foreach ($channelLimitList as $k => $channel) {
            if ($channel['channelNo'] == $channelNo) {
                $status = $channel['status'];
                break;
            }
        }
        $statusTxt = ChannelInfoStatusEnum::born($status)->label();

        $data = [
            'mchNo'              => $mchNo,
            'channelNo'          => $channelNo,
            'status'             => $status,
            'statusTxt'          => $statusTxt,
            'dayCurrentCount'    => $dayCurrentCount,
            'dayCurrentAmount'   => $dayCurrentAmount,
            'monthCurrentCount'  => $monthCurrentCount,
            'monthCurrentAmount' => $monthCurrentAmount,
            'day'                => date('Y-m-d'),
            'month'              => date('Ym'),
        ];

        return CustomUtils::buildSuccess($data);
    }

    /**
     * 商户通道限额列表
     *
     * @param $mchNo
     *
     * @return array
     */
    protected function _getMchChannelLimitList ($mchNo) {
        return $this->ChannelInfoDao->readMchChannelLimitList($mchNo);
    }

    /**
     * 通道限额列表
     *
     * @param array $channelNoList
     *
     * @return array[]
     */
    protected function _getChannelLimitList (array $channelNoList) {


        // 通道列表
        $channelList = [];

        foreach ($channelNoList as $channelNo) {

            // 读取通道限额信息(缓存优先)
            $channel = $this->ChannelInfoDao->readChannel($channelNo);

            if (!$channel) {
                continue;
            }

            $channelList[$channelNo] = $channel;
        }

        return $channelList;
    }

}
